a tool for shared writing and social publishing
1import { Metadata } from "next";
2import * as Y from "yjs";
3import * as base64 from "base64-js";
4
5import type { Fact } from "src/replicache";
6import type { Attribute } from "src/replicache/attributes";
7import { YJSFragmentToString } from "src/utils/yjsFragmentToString";
8import { Leaflet } from "./Leaflet";
9import { scanIndexLocal } from "src/replicache/utils";
10import { getRSVPData } from "actions/getRSVPData";
11import { PageSWRDataProvider } from "components/PageSWRDataProvider";
12import { getPollData } from "actions/pollActions";
13import { supabaseServerClient } from "supabase/serverClient";
14import { get_leaflet_data } from "app/api/rpc/[command]/get_leaflet_data";
15import { NotFoundLayout } from "components/PageLayouts/NotFoundLayout";
16import { getPublicationMetadataFromLeafletData } from "src/utils/getPublicationMetadataFromLeafletData";
17import { FontLoader, extractFontsFromFacts } from "components/FontLoader";
18
19export const preferredRegion = ["sfo1"];
20export const dynamic = "force-dynamic";
21export const fetchCache = "force-no-store";
22
23type Props = {
24 // this is now a token id not leaflet! Should probs rename
25 params: Promise<{ leaflet_id: string }>;
26};
27export default async function LeafletPage(props: Props) {
28 let { result: res } = await get_leaflet_data.handler(
29 { token_id: (await props.params).leaflet_id },
30 { supabase: supabaseServerClient },
31 );
32 let rootEntity = res.data?.root_entity;
33 if (!rootEntity || !res.data || res.data.blocked_by_admin)
34 return (
35 <NotFoundLayout>
36 <p className="font-bold">Sorry, we can't find this leaflet!</p>
37 <p>
38 This may be a glitch on our end. If the issue persists please{" "}
39 <a href="mailto:contact@leaflet.pub">send us a note</a>.
40 </p>
41 </NotFoundLayout>
42 );
43
44 let [{ data }, rsvp_data, poll_data] = await Promise.all([
45 supabaseServerClient.rpc("get_facts", {
46 root: rootEntity,
47 }),
48 getRSVPData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)),
49 getPollData(res.data.permission_token_rights.map((ptr) => ptr.entity_set)),
50 ]);
51 let initialFacts = (data as unknown as Fact<Attribute>[]) || [];
52
53 // Extract font settings from facts for server-side font loading
54 const { headingFontId, bodyFontId } = extractFontsFromFacts(initialFacts as any, rootEntity);
55
56 return (
57 <>
58 {/* Server-side font loading with preload and @font-face */}
59 <FontLoader headingFontId={headingFontId} bodyFontId={bodyFontId} />
60 <PageSWRDataProvider
61 rsvp_data={rsvp_data}
62 poll_data={poll_data}
63 leaflet_id={res.data.id}
64 leaflet_data={res}
65 >
66 <Leaflet
67 initialFacts={initialFacts}
68 leaflet_id={rootEntity}
69 token={res.data}
70 initialHeadingFontId={headingFontId}
71 initialBodyFontId={bodyFontId}
72 />
73 </PageSWRDataProvider>
74 </>
75 );
76}
77
78export async function generateMetadata(props: Props): Promise<Metadata> {
79 let { result: res } = await get_leaflet_data.handler(
80 { token_id: (await props.params).leaflet_id },
81 { supabase: supabaseServerClient },
82 );
83 let rootEntity = res.data?.root_entity;
84 if (!rootEntity || !res.data) return { title: "Leaflet not found" };
85 let publication_data = getPublicationMetadataFromLeafletData(res.data);
86 if (publication_data) {
87 return {
88 title: publication_data.title || "Untitled",
89 description: publication_data.description,
90 };
91 }
92 let { data } = await supabaseServerClient.rpc("get_facts", {
93 root: rootEntity,
94 });
95 let initialFacts = (data as unknown as Fact<Attribute>[]) || [];
96 let scan = scanIndexLocal(initialFacts);
97 let firstPage =
98 scan.eav(rootEntity, "root/page")[0]?.data.value || rootEntity;
99 let pageType = scan.eav(firstPage, "page/type")[0]?.data.value || "doc";
100 let firstBlock, secondBlock;
101 if (pageType === "canvas") {
102 [firstBlock, secondBlock] = scan
103 .eav(firstPage, "canvas/block")
104 .map((b) => {
105 let type = scan.eav(b.data.value, "block/type");
106 if (!type[0]) return null;
107 return {
108 ...b.data,
109 type: type[0].data.value,
110 };
111 })
112 .filter((b) => b !== null)
113 .filter((b) => b.type === "text" || b.type === "heading")
114 .sort((a, b) => {
115 if (a.position.y === b.position.y) {
116 return a.position.x - b.position.x;
117 }
118 return a.position.y - b.position.y;
119 });
120 } else {
121 [firstBlock, secondBlock] = scan
122 .eav(firstPage, "card/block")
123 .map((b) => {
124 let type = scan.eav(b.data.value, "block/type");
125 return {
126 ...b.data,
127 type: type[0]?.data.value,
128 };
129 })
130
131 .filter((b) => b.type === "text" || b.type === "heading")
132 .sort((a, b) => (a.position > b.position ? 1 : -1));
133 }
134 let metadata: Metadata = { title: "Untitled Leaflet", description: " " };
135
136 let titleFact = initialFacts.find(
137 (f) => f.entity === firstBlock?.value && f.attribute === "block/text",
138 ) as Fact<"block/text"> | undefined;
139 if (titleFact) {
140 let doc = new Y.Doc();
141 const update = base64.toByteArray(titleFact.data.value);
142 Y.applyUpdate(doc, update);
143 let nodes = doc.getXmlElement("prosemirror").toArray();
144 metadata.title = YJSFragmentToString(nodes[0]);
145 }
146
147 let descriptionFact = initialFacts.find(
148 (f) => f.entity === secondBlock?.value && f.attribute === "block/text",
149 ) as Fact<"block/text"> | undefined;
150 if (descriptionFact) {
151 let doc = new Y.Doc();
152 const update = base64.toByteArray(descriptionFact.data.value);
153 Y.applyUpdate(doc, update);
154 let nodes = doc.getXmlElement("prosemirror").toArray();
155 metadata.description = YJSFragmentToString(nodes[0]);
156 }
157
158 return metadata;
159}